﻿using Fleck;
using FySystem.Extension;
using FySystem.Logs;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

/*
 * 2022-08,V1.0
websocket的Helper类，封装了订阅（通配符仅支持单级:#）、发布、接口调用；
订阅和发布都区分前端和后端，前端订阅接收后端发送的消息，后端订阅接收前端发送的消息，暂时不支持前端对前端或者后端对后端的订阅和发布；
用法如下：
listener = new WSocketListenerHelper(websocketUrl);
                listener.OnReceiveCallback = msg =>
                {
                    FyLogContextV2.WriteLog($"rev,websocket:{msg}");        //收到消息回调
                };
                listener.OnSendCallback = msg =>
                {
                    FyLogContextV2.WriteLog($"send,websocket:{msg}");       //发送消息回调
                };
                listener.OnWebSubscribeTopic = (msginfo, socket) =>
                {
                    //前端订阅主题回调
                    RefreshDeviceConfigs(msginfo);                          //订阅某个设备的实时数据时，从数据库中重新获取并刷新设备的配置
                };
                listener.OnCloseCallback = msginfo =>
                {
                    //websocket关闭回调

                };
                listener.Start();

                //后端订阅主题接口，供前端调用，区分大小写
                listener.SubscribeTopic("/api/adjust", WSocketApi.GetRealAdjust);
                listener.SubscribeTopic("/api/historyadjust", WSocketApi.GetHistoryAdjust);
                listener.SubscribeTopic("/api/historyevent", WSocketApi.GetHistoryEvent);
                listener.SubscribeTopic("/api/faultwave", WSocketApi.GetFaultWave);
                listener.SubscribeTopic("/api/wavecapture", WSocketApi.WaveCapture);
                listener.SubscribeTopic("/api/maxmin", WSocketApi.GetHistoryMaxMin);
                listener.SubscribeTopic("/api/historyalarm", WSocketApi.GetHistoryAlarm);
                listener.SubscribeTopic("/api/energyquality", WSocketApi.EnergyQuality);
                listener.SubscribeTopic("/api/thds", WSocketApi.GetTHDs);
                listener.SubscribeTopic("/api/thdwave", WSocketApi.GetTHDWave);
*/
public class WSocketListenerHelper
{
    private JsonSerializerOptions _jsnOpt = new JsonSerializerOptions() { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
    private List<IWebSocketConnection> _connections = new List<IWebSocketConnection>();                             //后续做安全校验，只有登录之后的连接才放这里，并且只有登录之后的连接才可以调用其它api，需要定时清理长期没有数据交互的

    /// <summary>
    /// 服务端
    /// </summary>
    public WebSocketServer WServer { get; private set; }
    /// <summary>
    /// 前端订阅的主题
    /// </summary>
    public ConcurrentDictionary<string, WSTopic> WebTopics { get; set; } = new ConcurrentDictionary<string, WSTopic>();
    /// <summary>
    /// 后端订阅的主题
    /// </summary>
    public ConcurrentDictionary<string, WSTopic> BackEndTopics { get; set; } = new ConcurrentDictionary<string, WSTopic>();

    /// <summary>
    /// 有客户端连上时回调
    /// </summary>
    public Action<IWebSocketConnection>? OnConnect { get; set; }
    /// <summary>
    /// 收到数据回调，用于打印日志，如果需要的话
    /// </summary>
    public Action<string>? OnReceiveCallback { get; set; }
    /// <summary>
    /// 发送数据回调，用于打印日志，如果需要的话
    /// </summary>
    public Action<string>? OnSendCallback { get; set; }
    /// <summary>
    /// 连接关闭回调
    /// </summary>
    public Action<IWebSocketConnection>? OnCloseCallback { get; set; }
    /// <summary>
    /// 连接抛错回调
    /// </summary>
    public Action<IWebSocketConnection, Exception> OnException { get; set; }
    /// <summary>
    /// 客户端订阅时回调
    /// </summary>
    public Action<WSMessageInfo, IWebSocketConnection>? OnWebSubscribeTopic { get; set; }
    /// <summary>
    /// 客户端发布消息时回调
    /// </summary>
    public Action<WSMessageInfo, IWebSocketConnection>? OnWebPublish { get; set; }

    /// <summary>
    /// 构造
    /// </summary>
    /// <param name="wsocketUrl"></param>
    public WSocketListenerHelper(string wsocketUrl)
    {
        try
        {
            WServer = new WebSocketServer(wsocketUrl);
        }
        catch (Exception)
        {
            throw;
        }
    }

    /// <summary>
    /// 开始监听
    /// </summary>
    /// <exception cref="NotImplementedException"></exception>
    internal void Start()
    {
        try
        {
            WServer.Start(socket =>
            {
                socket.OnOpen = () =>
                {
                    OnConnect?.Invoke(socket);
                };
                socket.OnClose = () =>
                {
                    RemoveSocket(socket);
                };
                socket.OnError = (ex) =>
                {
                    RemoveSocket(socket);       //保险起见一旦出错就移除
                    OnException?.Invoke(socket, ex);
                };
                socket.OnBinary = (bdata) =>
                {
                    /* 以下注释摘自另一个物联网项目，因此描述上和这个项目有些对不上，但意思是一样的
                     * message需要有固定的格式，为json字符串：
                     * command：指令，数字格式；
                     * topic：订阅或发布的主题；
                     * reqid：请求id，仅当command为3时有效
                     * status: 状态代码，0表示成功，1表示失败，仅当command为3时有效
                     * err: 错误，仅当command为3时有效
                     * val：内容，字符串，格式任意，可以是普通字符串，也可以是json字符串，根据实际情况决定
                     * 先定以下几种：
                     * 1.订阅，command为0，例：
                     *      command：0，topic："real/9001001"，表示订阅一个名为real/9001001的主题
                     * 2.发布，command为1，例：
                     *      command：1，topic："setadjust/9001001", val: "{\"pro_longtime\":\"0\"}"
                     *      发布主题为“setadjust/9001001”的数据，内容为val中的字符串
                     * 3.类似调用接口的，发布了一个主题的消息后，需要服务端马上返回结果，command为2，
                     *      注：这个最好是用回调，但c++和qt才刚刚学，不会做回调；
                     *      例：
                     *      command：2，topic："getdeviceinfo"，reqid："47389210"，val:"9001001"
                     *      服务端收到消息后，马上返回相应的结果：
                     *      command：3，topic："getdeviceinfo"，reqid："47389210"，status:0，err: ""，val:"{\"sn\":"9001001",\"u\":220,\"i\":20.5}"
                     *      返回的reqid和请求时发送的reqid相同，是一个随机数或者guid，这样不至于连续两次调用getdeviceinfo时，不知道服务端
                     *      返回的数据是对应哪一次请求的，status为状态，0表示成功，其它表示失败，
                     *      err为错误消息，当status为0时err字段为空，status为其它值时，err为错误的文本描述。
                     * */
                    //防止不同操作系统上出现乱码，统一用UTF8
                    if (bdata[0] == 0x01)
                    {
                        //如果是文本消息
                        string message = Encoding.UTF8.GetString(bdata.AsSpan().Slice(1));
                        ProcessMessage(message, socket);
                    }
                    else if (bdata[1] == 0x02)
                    {
                        //如果是二进制消息
                        //to-do
                    }
                };
            });
        }
        catch (Exception)
        {
            throw;
        }
    }

    /// <summary>
    /// 回收垃圾
    /// </summary>
    /// <param name="socket"></param>
    /// <exception cref="NotImplementedException"></exception>
    private void RemoveSocket(IWebSocketConnection socket)
    {
        try
        {
            //关闭回调
            OnCloseCallback?.Invoke(socket);

            string[] keys = WebTopics.Keys.ToArray();
            if (keys == null)
                return;
            if (keys.Length <= 0)
                return;

            int totalSocs = 0;
            foreach(string key in keys)
            {
                var topic = WebTopics[key];
                topic.RemoveSocket(socket);
                totalSocs += topic.WSConnections.Count;

                //如果该主题下已经没有客户端连接了，则将该主题移除
                if (topic.WSConnections.IsNullOrEmpty())
                    WebTopics.Remove(key, out topic);
            }

            FyLogContextV2.WriteLog($"wsocketcnt:{totalSocs}个");
        }
        catch (Exception)
        {
            throw;
        }
    }

    /// <summary>
    /// 对收到的消息进行处理
    /// </summary>
    /// <param name="message"></param>
    /// <exception cref="NotImplementedException"></exception>
    private void ProcessMessage(string message, IWebSocketConnection socket)
    {
        try
        {
            if (OnReceiveCallback != null)
                OnReceiveCallback.Invoke(message);

            WSMessageInfo msgInfo = JsonSerializer.Deserialize<WSMessageInfo>(message, _jsnOpt);
            if (msgInfo == null)
                throw new Exception("WSocketListenerHelper.ProcessMessage#收到数据为空");
            
            //为了执行效率，舍弃工厂模式
            if (msgInfo.command == 0)
            {
                //订阅主题
                ProcessSubscribe(msgInfo, socket);
            }
            else if (msgInfo.command == 1)
            {
                //发布主题
                ProcessPublish(msgInfo, socket);
            }
            else if (msgInfo.command == 2)
            {
                //调用接口
                ProcessCallApi(msgInfo, socket);
            }
            else if (msgInfo.command == 3)
            {
                //这个暂时服务器这边不会有，是服务器发送给前端的
            }
        }
        catch (Exception ex)
        {
            FyLogContextV2.WriteError(ex.Message);
        }
    }

    /// <summary>
    /// 前端订阅主题处理
    /// </summary>
    /// <param name="msgInfo"></param>
    /// <exception cref="NotImplementedException"></exception>
    private void ProcessSubscribe(WSMessageInfo msgInfo, IWebSocketConnection socket)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(msgInfo.topic))
                return;

            OnWebSubscribeTopic?.Invoke(msgInfo, socket);

            WSTopic topic = null;
            if(WebTopics.TryGetValue(msgInfo.topic, out topic))
            {
                //如果原本就有同名主题
                topic.AddSocket(socket);
            }
            else
            {
                //如果原本没有同名主题
                topic = new WSTopic(msgInfo.topic, socket);
                if(WebTopics.TryAdd(msgInfo.topic, topic) == false)
                {
                    //极端情况下判断的时候字典里还没有，但在上面一行添加的时候，在另一个线程中已经将同名主题添加进去了
                    WebTopics[msgInfo.topic] = topic;
                }
            }
        }
        catch (Exception)
        {
            throw;
        }
    }

    /// <summary>
    /// 前端发布消息
    /// </summary>
    /// <param name="msgInfo"></param>
    /// <param name="socket"></param>
    /// <exception cref="NotImplementedException"></exception>
    private void ProcessPublish(WSMessageInfo msgInfo, IWebSocketConnection socket)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(msgInfo.topic))
                return;

            OnWebPublish?.Invoke(msgInfo, socket);

            //找出匹配的后端主题
            var values = BackEndTopics.Values.ToArray();
            List<WSTopic> matchedTopics = GetMatchedTopics(msgInfo.topic, values);

            if (matchedTopics.Count <= 0)
                return;

            //执行后端的函数
            foreach(var topic in matchedTopics)
            {
                foreach(var action in topic.SubscribAction)
                {
                    action.Invoke(msgInfo, socket);
                }
                foreach(var fun in topic.SubscribeFun)
                {
                    fun.Invoke(msgInfo, socket);
                }
            }
        }
        catch (Exception)
        {
            throw;
        }
    }

    /// <summary>
    /// 前端调用接口
    /// </summary>
    /// <param name="msgInfo"></param>
    /// <param name="socket"></param>
    /// <exception cref="NotImplementedException"></exception>
    private void ProcessCallApi(WSMessageInfo msgInfo, IWebSocketConnection socket)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(msgInfo.topic))
                return;

            //找出匹配的后端主题
            var values = BackEndTopics.Values.ToArray();
            List<WSTopic> matchedTopics = GetMatchedTopics(msgInfo.topic, values);

            if (matchedTopics.Count <= 0)
                return;

            //执行后端的函数
            foreach (var topic in matchedTopics)
            {
                //类似调用接口的，如果有返回值
                foreach (var fun in topic.SubscribeFun)
                {
                    //执行函数并将结果返回数据给前端
                    WSMessageInfo msg = new WSMessageInfo();
                    msg.command = 3;
                    msg.topic = msgInfo.topic;
                    msg.reqid = msgInfo.reqid;
                    msg.status = 0;

                    try
                    {
                        object obj = fun.Invoke(msgInfo, socket);

                        string jsn = "";
                        if (obj != null)
                            jsn = JsonSerializer.Serialize(obj, _jsnOpt);

                        msg.val = jsn;
                    }
                    catch (Exception ex)
                    {
                        msg.status = 1;
                        msg.err = ex.Message;
                    }

                    Send(msg, socket);
                }
                //如果没有返回值
                foreach(var fun in topic.SubscribAction)
                {
                    WSMessageInfo msg = new WSMessageInfo();
                    msg.command = 3;
                    msg.topic = msgInfo.topic;
                    msg.reqid = msgInfo.reqid;
                    msg.status = 0;

                    try
                    {
                        fun.Invoke(msgInfo, socket);
                    }
                    catch (Exception ex)
                    {
                        msg.status = 1;
                        msg.err = ex.Message;
                    }

                    Send(msg, socket);
                }
            }
        }
        catch (Exception)
        {
            throw;
        }
    }

    /// <summary>
    /// 给前端发送数据
    /// </summary>
    /// <param name="msg"></param>
    /// <param name="socket"></param>
    private void Send(WSMessageInfo msg, IWebSocketConnection socket)
    {
        try
        {
            string jsn = JsonSerializer.Serialize(msg, _jsnOpt);
            if (OnSendCallback != null)
                OnSendCallback.Invoke(jsn);

            List<byte> bdata = new List<byte>();
            bdata.Add(0x01);
            bdata.AddRange(Encoding.UTF8.GetBytes(jsn));

            socket.Send(bdata.ToArray());
        }
        catch (Exception ex)
        {
            FyLogContextV2.WriteError(ex.Message);
        }
    }

    /// <summary>
    /// 找出匹配的主题列表
    /// </summary>
    /// <param name="topic"></param>
    /// <param name="values"></param>
    /// <returns></returns>
    /// <exception cref="NotImplementedException"></exception>
    private List<WSTopic> GetMatchedTopics(string topic, WSTopic[] values)
    {
        List<WSTopic> res = new List<WSTopic>();
        try
        {
            if (values.IsNullOrEmpty())
                return res;

            foreach(var item in values)
            {
                if (item.IsMatch(topic))
                {
                    item.CommunicationTime = DateTime.Now;
                    res.Add(item);
                }
            }
        }
        catch (Exception)
        {
            throw;
        }
        return res;
    }

    //************************************** 后端接口 ******************************************
    /// <summary>
    /// 后端订阅接口
    /// </summary>
    /// <param name="topic"></param>
    public void SubscribeTopic(string topic,Func<WSMessageInfo, IWebSocketConnection, object> callback)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(topic))
                return;
            if (callback == null)
                return;

            WSTopic tmp = null;
            if(BackEndTopics.TryGetValue(topic,out tmp))
            {
                //如果已有同名主题
                tmp.SubscribeFun.Add(callback);
            }
            else
            {
                //如果不存在同名主题，新增
                tmp = new WSTopic(topic, callback);
                if(BackEndTopics.TryAdd(topic, tmp) == false)
                {
                    BackEndTopics[topic] = tmp;
                }
            }
        }
        catch (Exception)
        {
            throw;
        }
    }

    /// <summary>
    /// 后端订阅消息
    /// </summary>
    /// <param name="topic"></param>
    /// <param name="callback"></param>
    public void SubscribeTopic(string topic,Action<WSMessageInfo, IWebSocketConnection> callback)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(topic))
                return;
            if (callback == null)
                return;

            WSTopic tmp = null;
            if(BackEndTopics.TryGetValue(topic,out tmp))
            {
                //如果已有主题
                tmp.SubscribAction.Add(callback);
            }
            else
            {
                //如果原本不存在同名主题，新增
                tmp = new WSTopic(topic, callback);
                if(BackEndTopics.TryAdd(topic,tmp) == false)
                {
                    BackEndTopics[topic] = tmp;
                }
            }
        }
        catch (Exception)
        {
            throw;
        }
    }

    /// <summary>
    /// 判断是否有匹配的主题，有时候可能组要发送的数据包比较耗时，可以先判断是否需要推送，然后再组包
    /// </summary>
    /// <param name="topic"></param>
    /// <returns></returns>
    public bool HasMatchedTopic(string topic)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(topic))
                throw new Exception("topic不能为空~");

            //找出匹配后的前端主题
            var values = WebTopics.Values.ToArray();
            List<WSTopic> matchedTopics = GetMatchedTopics(topic, values);

            if (matchedTopics.Count <= 0)
                return false;
            else
                return true;
        }
        catch (Exception)
        {
            throw;
        }
    }

    /// <summary>
    /// 发布消息
    /// </summary>
    /// <param name="topic"></param>
    /// <param name="msg"></param>
    public void PublishTopic(string topic,string msg)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(topic))
                throw new Exception("topic不能为空~");

            //找出匹配后的前端主题
            var values = WebTopics.Values.ToArray();
            List<WSTopic> matchedTopics = GetMatchedTopics(topic, values);

            if (matchedTopics.Count <= 0)
                return;

            //向前端发送别的数据
            foreach(var tp in matchedTopics)
            {
                WSMessageInfo msgInfo = new WSMessageInfo();
                msgInfo.command = 1;
                msgInfo.topic = topic;
                msgInfo.val = msg;

                foreach (var socket in tp.WSConnections)
                {
                    Send(msgInfo, socket);
                }
            }
        }
        catch (Exception)
        {
            throw;
        }
    }
}

public class WSMessageInfo
{
    public int command { get; set; }
    public string topic { get; set; }
    public string reqid { get; set; }
    public int status { get; set; }
    public string err { get; set; }
    public string val { get; set; }
}

/// <summary>
/// 主题
/// </summary>
public class WSTopic
{
    /// <summary>
    /// 按照/分段后主题
    /// </summary>
    private string[] sptopic { get; set; }
    /// <summary>
    /// 主题
    /// </summary>
    public string? topic { get; private set; }
    /// <summary>
    /// 订阅该主题的是网页端还是后端，true表示网页端
    /// </summary>
    public bool? IsWebClient { get; private set; } = true;
    /// <summary>
    /// 有返回值的后端调用函数，当收到前端发送的对应消息后，调用并返回结果，类似于接口调用
    /// </summary>
    public List<Func<WSMessageInfo, IWebSocketConnection, object>> SubscribeFun { get; private set; } = new List<Func<WSMessageInfo, IWebSocketConnection, object>>();
    /// <summary>
    /// 没有返回值的后端调用函数，当收到前端发送的对应消息后，调用
    /// </summary>
    public List<Action<WSMessageInfo, IWebSocketConnection>> SubscribAction { get; private set; } = new List<Action<WSMessageInfo, IWebSocketConnection>>();
    /// <summary>
    /// WebSocket的连接列表
    /// </summary>
    public List<IWebSocketConnection> WSConnections { get; set; } = new List<IWebSocketConnection>();
    /// <summary>
    /// 主题发生发布的时间，为了防止内存泄漏，后续可能会通过该字段来做垃圾回收
    /// </summary>
    public DateTime CommunicationTime { get; set; } = DateTime.Now;

    /// <summary>
    /// 构造，前端订阅主题，方便起见，topic只有单段的通配符#，也就是real/#能匹配real/9001，但不能匹配real/9001/xyz
    /// </summary>
    /// <param name="topic"></param>
    public WSTopic(string? topic, IWebSocketConnection socket)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(topic))
                throw new Exception("topic不能为空~");

            topic = topic.Trim();
            sptopic = topic.Split('/');
            sptopic = sptopic.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();      //把空字符串过滤掉

            AddSocket(socket);
        }
        catch (Exception)
        {
            throw;
        }
    }

    public WSTopic(string? topic, Func<WSMessageInfo, IWebSocketConnection, object>? subscribeFun)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(topic))
                throw new Exception("topic不能为空~");

            IsWebClient = false;
            topic = topic.Trim();
            sptopic = topic.Split('/');
            sptopic = sptopic.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();      //把空字符串过滤掉

            SubscribeFun.Add(subscribeFun);
        }
        catch (Exception)
        {
            throw;
        }
    }

    public WSTopic(string? topic, Action<WSMessageInfo, IWebSocketConnection>? subscribeAction)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(topic))
                throw new Exception("topic不能为空~");

            IsWebClient = false;
            topic = topic.Trim();
            sptopic = topic.Split('/');
            sptopic = sptopic.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();      //把空字符串过滤掉

            SubscribAction.Add(subscribeAction);
        }
        catch (Exception)
        {
            throw;
        }
    }

    /// <summary>
    /// 添加websocket
    /// </summary>
    /// <param name="socket"></param>
    /// <exception cref="NotImplementedException"></exception>
    internal void AddSocket(IWebSocketConnection socket)
    {
        try
        {
            lock (WSConnections)
            {
                if (WSConnections.Contains(socket) == false)
                    WSConnections.Add(socket);
            }
        }
        catch (Exception)
        {
            throw;
        }
    }

    /// <summary>
    /// 移除连接
    /// </summary>
    /// <param name="socket"></param>
    /// <exception cref="NotImplementedException"></exception>
    internal void RemoveSocket(IWebSocketConnection socket)
    {
        try
        {
            lock (WSConnections)
                WSConnections.Remove(socket);
        }
        catch (Exception)
        {
            throw;
        }
    }

    /// <summary>
    /// 判断是否匹配指定的主题
    /// </summary>
    /// <param name="topic"></param>
    /// <returns></returns>
    /// <exception cref="NotImplementedException"></exception>
    internal bool IsMatch(string topic)
    {
        bool res = true;
        try
        {
            string[] sources = topic.Split('/');
            sources = sources.Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();
            if (sources.IsNullOrEmpty())
                return false;

            //简单起见，由于没有多段通配符，所以长度不同肯定匹配不上
            if (sources.Length != sptopic.Length)
                return false;

            for (int i = 0; i < sources.Length; i++)
            {
                if (sources[i] != sptopic[i])
                {
                    if (sources[i]!="#" && sptopic[i] != "#")
                    {
                        res = false;
                        break;
                    }
                }
            }
        }
        catch (Exception)
        {
            throw;
        }
        return res;
    }
}